GASでデータベース内へのページ挿入とテーブル追加を自動処理する #Notion
毎週定期実施していたプロセスのうち、GASにてSpreadSheet上に出力したデータをNotion上に手作業でコピペする手続きが部署内再編に伴って見直しとなりました。Slackへ貼り付けてのフローデータ扱いも考えましたが表形式のデータであるため整形しづらく、Notionにテーブル化したほうが楽そうです。
よくある自動化の手続きですが、手順のサンプルとして挙げてみました。
前提
プロセスの自動化は以前でもGAS内からNotionAPIも実行することで十分可能な見込みだったものの、課題はデータの貼り付け先です。2つの既存Blockの間になるのですが、Notion APIは一番下への追加になる仕組みです。一筋縄では行きませんでした。
幸いにも見直しで2つの既存Blockの間へ挟むプロセスを必ずしも踏襲する必要はなくなりました。ただし、所謂スナップショットの扱いとなっていて、先週分とデータを混在させることは想定されていません。常に独立させる必要があります。
Notionへの出力
表組みでデータ量は少ないものの、追加日などメタ情報の管理が必要です。メタ情報はプロパティ管理の方が都合良さそうなのでデータベースを一つ設け、データは毎回データベース内に作成したレコード内へ独立したテーブルとしての追加で試しました。
Notion APIのパラメータ構成
コネクトを設定する都合上、データベースは事前に追加します。呼び出すのは以下2つのAPIとなります。
- ページ追加API
- テーブル追加API
ページ追加APIパラメータ
今回のページ追加APIは親ページがデータベースになるため、パラメータにはデータベースのプロパティ構成が求められます。データベースに存在しないプロパティデータを指定するとエラーになります。
https://api.notion.com/v1/pages
{
"parent": { "database_id": database_id },
"properties": {
"件名": {
"title": [
{
"text": {
"content": Utilities.formatDate(new Date(), 'JST', 'yyyy-MM-dd HH:mm:ss ')
}
}
]
},
"完了": {
"rich_text": [
{
"text": {
"content": String(Math.floor(status.完了))
}
}
]
},
"処理中": {
"rich_text": [
{
"text": {
"content": String(Math.floor(status.処理中))
}
}
]
},
"未対応": {
"rich_text": [
{
"text": {
"content": String(Math.floor(status.未対応))
}
}
]
},
}
}
テーブル追加API
テーブル追加APIではテーブル構成とレコードの2つを構成しますが、1回のAPI呼び出しで構成できます。
注意すべきとして、table_width
の指定は作成時のみ可能で後から変更不可能になります。
行データはchildren
内にtable_row
の配列構成とし、table_row
にはCell用テキストブロックが収まったcells
配列を収めます。ヘッダ指定はhas_column_header
とhas_row_header
へのフラグによるものです。
https://api.notion.com/v1/blocks/<RecordId>/children
{
"children": [
{
"object": "block",
"type": "table",
"table": {
"table_width": 4,
"has_column_header": true,
"has_row_header": false,
"children": [
{
"table_row": {
"cells": [
[
{
"type": "text",
"text": {
"content": "キー",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "キー",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "件名",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "件名",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "状態",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "状態",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "種別",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "種別",
"href": null
}
],
]
}
},
{
"table_row": {
"cells": [
[
{
"type": "text",
"text": {
"content": "TEST-1",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "TEST-1",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "テスト",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "テスト",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "未対応",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "未対応",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "タスク",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "タスク",
"href": null
}
]
]
}
}
]
}
}
]
};
GASでの実行
金曜日に実行して、今週発行されたチケットを取得する想定としています。
var prop = PropertiesService.getScriptProperties();
const notionToken = prop.getProperty('NOTION_KEY')
const apiKey = prop.getProperty('BACKLOG_API_KEY');
const spaceId = prop.getProperty('BACKLOG_SPACE_ID');
const projectId = prop.getProperty('PROJECT_ID');
var BACKLOG_ISSUE_ENDPOINT = "https://" + spaceId + ".backlog.com/api/v2/issues";
var BACKLOG_VIEW_URL = "https://" + spaceId + ".backlog.com/view/";
function getData() {
var d = new Date();
d.setDate(d.getDate() - Number(4));
var since = Utilities.formatDate(d, 'JST', 'yyyy-MM-dd');
var res = UrlFetchApp.fetch(BACKLOG_ISSUE_ENDPOINT + "?apiKey=" + apiKey + "&projectId[]=" + projectId + "&sort=updated&updatedSince=" + since);
var contents = JSON.parse(res.getContentText());
var values = contents.map(function (info) {
return [
{"text": info.issueKey, "url": true},
{"text": info.summary},
{"text": info.status.name},
{"text": info.issueType.name}
]
});
return values
}
function getStatusCount(dataArray) {
var result = {"未対応": 0, "処理中": 0, "完了": 0}
for (i=0;i<dataArray.length;i++) {
var data = dataArray[i];
switch (true) {
case data[2].text === "完了":
result.完了 = result.完了 + 1
break;
case data[2].text === "処理中":
result.処理中 = result.処理中 + 1
break;
case data[2].text === "未対応":
result.未対応 = result.未対応 + 1
break;
default:
break;
}
}
return result
}
function createRecord(data) {
var api = 'https://api.notion.com/v1/pages';
var database = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
var status = getStatusCount(data)
var data = {
"parent": { "database_id": database },
"properties": {
"件名": {
"title": [
{
"text": {
"content": Utilities.formatDate(new Date(), 'JST', 'yyyy-MM-dd HH:mm:ss ')
}
}
]
},
"完了": {
"rich_text": [
{
"text": {
"content": String(Math.floor(status.完了))
}
}
]
},
"処理中": {
"rich_text": [
{
"text": {
"content": String(Math.floor(status.処理中))
}
}
]
},
"未対応": {
"rich_text": [
{
"text": {
"content": String(Math.floor(status.未対応))
}
}
]
},
}
}
var result = callAPI("post", api, data)
return result.id
}
function addTable(recordId, dataArray) {
var apiUrl = "https://api.notion.com/v1/blocks/" + recordId + "/children";
var data = {
"children": [
{
"object": "block",
"type": "table",
"table": {
"table_width": 4,
"has_column_header": true,
"has_row_header": false,
"children": []
}
}
]
}
var titleText = [{"text": "キー"}, {"text": "件名"}, {"text": "状態"}, {"text": "種別"}];
data.children[0].table.children.push(rowData(titleText))
for (i=0;i<dataArray.length;i++) {
data.children[0].table.children.push(rowData(dataArray[i]))
}
callAPI("patch", apiUrl, data)
}
function rowData(texts) {
var row = {"table_row": {"cells": []}};
for (i=0;i<texts.length;i++) {
row.table_row.cells.push(cellValue(texts[i]))
}
return row
}
function cellValue(data) {
return [{
"type": "text",
"text": {
"content": data.text,
"link": "url" in data && data.url ? { "url": getBackLogKeyUrl(data.text) } : null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": data.text,
"href": "url" in data && data.url ? getBackLogKeyUrl(data.text) : null
}]
}
function getBackLogKeyUrl(key) {
return backlog_url + key
}
function callAPI(method, api, data) {
const options = {
"method": method,
"headers": {
'Authorization': 'Bearer ' + notionToken,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
},
"payload": JSON.stringify(data),
"muteHttpExceptions": true
};
var response = UrlFetchApp.fetch(api, options);
var responseData = JSON.parse(response);
Logger.log(responseData)
return responseData;
}
function main() {
var data = getData()
var recordId = createRecord(data)
addTable(recordId, data)
}
実行すると以下のようなページ及びテーブルが出力されます。キーには課題ページへのリンクが設定されています。
あとがき
データベースのプロパティにステータスのカウントを入れているのは、各週のプロパティを元にグラフ化することを想定したものです。
NotionAPIで厄介なのは、自在に型を変えられるブロックの入れ子構成が求められるものの、型によってデータ構成もまた変わってくるところです。APIドキュメントを参考にしても上手くいかない場合は、API実行ログ中に求められているkeyなどを元にパラメータ構成を変えてみるとよいでしょう。